<template>
  <div class="graphContainer" ref="mountNode">
    <i class="el-icon-full-screen fangda" @click="fangda"></i>
    <div
      :id="treeId"
      style="height: 100%; width: 100%; background-color: #fff"
      ref="parentContent"
    ></div>
    <el-dialog title="节点信息" :visible.sync="formDialog" width="40%">
      <el-form size="mini">
        <el-form-item label="描述">
          <el-input v-model="nodeForm.label"></el-input>
        </el-form-item>
        <el-form-item label="宽">
          <el-col :span="11">
            <el-input
              placeholder="请输入不小于20的数字"
              v-model="nodeForm.width"
              @input="validwidth"
              @blur="widthBlur"
            ></el-input>
          </el-col>
          <el-col class="line" :span="2"></el-col>
          <el-col :span="11"></el-col>
        </el-form-item>
        <el-form-item label="高">
          <el-col :span="11">
            <el-input
              placeholder="请输入不小于50的数字"
              v-model="nodeForm.height"
              @input="validheight"
              @blur="heightBlur"
            ></el-input>
          </el-col>
          <el-col class="line" :span="2"></el-col>
          <el-col :span="11"></el-col>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button size="mini" @click="formDialog = false">取 消</el-button>
        <el-button size="mini" type="primary" @click="commit">
          确 定
        </el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import G6 from "@antv/g6";
import screenfull from "screenfull";
import insertCss from "insert-css";

export default {
  name: "G6Container",
  props: {
    // 节点dom id 防止一个页面引入两次组件而出现同样的dom id
    treeId: {
      type: String,
      default: "mountNode",
    },
    treedata: {
      required: true,
    },
    treedataId: {
      // 页面左侧选择节点id
      type: String,
      default: "",
    },
    numInput: {
      required: false, //编辑弹框数值是否能输入
    },
    // 是否右键显示操作按钮
    operate: {
      type: Boolean,
      default: false,
    },
    showNum: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      myTree: null,
      currentSelectNode: null,
      graph: null,
      currIsFull: false,
      actType: "", // 操作类型，有添加下级 删除节点 和 编辑 三种操作
      formDialog: false,
      currNode: null, // 当前节点 添加和编辑的时候会用到
      nodeForm: {
        num: "",
        label: "",
        width: 120,
        height: 50,
      },
      width: 0,
      height: 0,
    };
  },
  watch: {
    // 监听 obj 的变化，包括其嵌套属性的变化
    treedata: {
      handler(newVal, oldVal) {
        this.myTree = newVal;
        this.graph.changeData(this.myTree);
      },
      deep: false, // 深度监听
      immediate: false, // 立即执行一次监听函数
    },
  },

  created() {
    // 这是 鼠标滑入时 弹窗的样式，官网示例是这样的  需要安装引入 inserCss
    insertCss(`
      .g6-tooltip {
        border: 1px solid #e2e2e2;
        border-radius: 4px;
        font-size: 12px;
        color: #545454;
        background-color: rgba(255, 255, 255, 0.9);
        padding: 10px 8px;
        box-shadow: rgb(174, 174, 174) 0px 0px 10px;
      }
    `);
    // 这是右键点击节点出现的弹窗的样式
    insertCss(`
      .g6-component-contextmenu {
        position: absolute;
        list-style-type: none;
        padding: 10px 8px;
        left: -150px;
        background-color: rgba(255, 255, 255, 0.9);
        border: 1px solid #e2e2e2;
        border-radius: 4px;
        font-size: 12px;
        color: #545454;
      }
      .g6-component-contextmenu ul {
        padding: 0;
        margin: 0;
      }
      .g6-component-contextmenu li {
        cursor: pointer;
        list-style-type:none;
        list-style: none;
        margin-left: 0px;
      }
      .g6-component-contextmenu li:hover {
        color: #aaa;
      }
    `);
  },
  mounted() {
    const container = document.getElementById(this.treeId);
    this.width = container.scrollWidth;
    this.height = container.scrollHeight;
    this.myTree = this.treedata;
    this.initGraph(this.myTree);
    // 添加 浏览器窗口改变大小时的监听
    this.initSize();
    // }
  },
  methods: {
    initSize() {
      const self = this; // 因为箭头函数会改变this指向，指向windows。所以先把this保存
      setTimeout(() => {
        // todo 浏览器窗口发生变化时
        window.onresize = function () {
          // todo 获取div parentContent 的宽度和高度
          this.canvasWidth = self.$refs.mountNode.clientWidth;
          this.canvasHeight = self.$refs.mountNode.clientHeight;
          // todo 修改画布的大小
          self.graph.changeSize(this.canvasWidth, this.canvasHeight);
          // todo 将图移动到画布中心位置
          self.graph.fitCenter();
        };
      }, 20);
    },
    validwidth(value) {
      // 使用Number函数将输入转换为数字，如果转换失败则value为NaN
      const parsedValue = Number(value);
      if (parsedValue.length > 1) {
        // 检查转换是否成功，并且值不小于20
        if (isNaN(parsedValue) || parsedValue < 20) {
          // 如果不满足条件，将inputValue重置为20
          this.$set(this.nodeForm, "width", 20);
          // 可以选择性地显示一个错误消息
          this.$message.error("请输入不小于20的有效数字！");
        } else {
          // 如果满足条件，更新inputValue的值
          this.$set(this.nodeForm, "width", parsedValue);
        }
      }
    },
    widthBlur() {
      if (this.nodeForm.width < 20) {
        this.$set(this.nodeForm, "width", 20);
        // 可以选择性地显示一个错误消息
        this.$message.error("请输入不小于20的有效数字！");
      }
    },
    validheight(value) {
      // 使用Number函数将输入转换为数字，如果转换失败则value为NaN
      const parsedValue = Number(value);
      if (parsedValue.length > 1) {
        // 检查转换是否成功，并且值不小于50
        if (isNaN(parsedValue) || parsedValue < 50) {
          // 如果不满足条件，将inputValue重置为50
          this.$set(this.nodeForm, "height", 50);
          // 可以选择性地显示一个错误消息
          this.$message.error("请输入不小于50的有效数字！");
        } else {
          // 如果满足条件，更新inputValue的值
          this.$set(this.nodeForm, "height", parsedValue);
        }
      }
    },
    heightBlur() {
      if (this.nodeForm.height < 50) {
        this.$set(this.nodeForm, "height", 50);
        // 可以选择性地显示一个错误消息
        this.$message.error("请输入不小于50的有效数字！");
      }
    },
    commit() {
      if (this.actType === "ADDCHILD") {
        let id = this.generateUniqueId(this.myTree);
        this.$set(this.nodeForm, "id", id);
        this.$set(this.nodeForm, "size", [
          Number(this.nodeForm.width),
          Number(this.nodeForm.height),
        ]);
        this.addChildNode(this.myTree, this.currNode, this.nodeForm);
        if (this.myTree.id === this.currNode.id) {  // 根节点加
          this.graph.changeData(this.myTree);
          this.$emit("addChild", {
            id: this.currNode.id,
            childNode: this.nodeForm
          });
        }else {
          this.$emit("addChildAndupdata", {
            id: this.currNode.id,
            childNode: this.nodeForm
          });
        }
      } else if (this.actType === "EDITITEM") {
        // 编辑后赋值
        this.$set(this.nodeForm, "size", [
          Number(this.nodeForm.width),
          Number(this.nodeForm.height),
        ]);
        this.updateNode(this.myTree, this.currNode.id, this.nodeForm);
        
        if (this.myTree.width || this.myTree.height) {
          this.myTree.width = Number(this.myTree.width);
          this.myTree.height = Number(this.myTree.height);
        }
        this.getNewTree(this.myTree); //节点的width和height必须为number

        setTimeout(() => {
          this.graph.updateItem(this.currNode.id, this.nodeForm, false);
        }, 200);
      }

      this.formDialog = false;
    },
    resize() {
      this.currIsFull = screenfull.isFullscreen;
      if (this.currIsFull) {
        this.graph.changeSize(window.innerWidth, window.innerHeight);
      } else {
        this.graph.changeSize(this.width, this.height);
      }
    },
    resizeListener() {
      window.addEventListener("resize", this.resize);
    },
    fangda() {
      this.currIsFull = !this.currIsFull;
      screenfull.toggle(this.$refs[this.treeId]);
      this.resize();
    },
    initMenu() {
      this.contextMenu = new G6.Menu({
        getContent(evt) {
          return `
          <ul>
            <li id="child">添加下级</li>
            <li id="delete">删除节点</li>
            <li id="edit">编辑</li>
          </ul>`;
        },
        handleMenuClick: (target, item) => {
          let name = target.id;
          let model = item._cfg.model;
          if (name === "child") {
            this.currNode = model;
            this.addChild();
          } else if (name === "edit") {
            this.currNode = model;
            this.editItem();
          } else if (name === "delete") {
            this.currNode = model;
            this.delItem();
          }
        },
        // 显示菜单时 的坐标偏移量
        offsetX: -5,
        // 显示菜单时 的坐标偏移量
        offsetY: -5,
        // 在哪些类型的元素上响应
        itemTypes: ["node"],
      });
    },
    initGraph(data) {
      // 注册节点 自定义样式
      this.registerNode();
      // 注册边 自定义边的路线
      this.registerEdge();
      if (this.operate) {
        this.initMenu();
      }
      // 创建 树形图  关于树形，具体描述可以去官网查
      this.graph = new G6.TreeGraph({
        container: this.treeId,
        // 线是否连在中间，
        linkCenter: true,
        // 右键菜单
        plugins: [this.operate ? this.contextMenu : ""],
        // 绘制动画
        animate: true,
        // 适应界面
        fitView: true,
        // maxZoom: 1, // 最大比例
        // minZoom: 0.5, // 最小比例
        // fitViewPadding: [0, 0, 0, 0], // 边距  fitView: true时生效  根据需要使用
        width: this.width,
        autoPaint: true,
        height: this.height,
        // 画在中间
        fitCenter: true,
        // 拖拽节点 拖拽画布 缩放画布 鼠标滑入的弹窗内容
        modes: {
          default: [
            "drag-node",
            "drag-canvas",
            "zoom-canvas",
            {
              type: "tooltip",
              formatText: function formatText(model) {
                const text = `名称: ${model.label}`;
                return text;
              },

              shouldUpdate: function shouldUpdate() {
                return true;
              },
            },
          ],
        },
        renderer: "canvas", // 渲染方式
        enabledStack: false, // 是否开启 redo & undo 功能
        // 默认节点类型
        defaultNode: {
          shape: "card-node",
          type: "card-node",
        },
        // 默认边类型
        defaultEdge: {
          shape: "card-edge",
          type: "card-edge",
          style: {
            stroke: "#91d5ff",
          },
        },
        // 布局的类型
        layout: {
          type: "compactBox",
          direction: "TB",
          getId: function getId(d) {
            return d.id;
          },
          // 每个节点的垂直间隙
          getVGap: function getVGap(e) {
            return e.size[1];
          },
          // 每个节点的水平间隙
          getHGap: function getHGap(e) {
            return e.size[0];
          },
        },
        scroller: {
          enabled: true,
          pannable: true,
          pageVisible: true,
          pageBreak: false,
        },
        mousewheel: {
          enabled: true,
          global: true,
          modifiers: ["ctrl", "meta"],
        },
      });
      // this.graph.positionsAnimate();
      this.graph.data(data);
      this.graph.render();
      // 适应 dom 容器
      this.graph.fitView();
    },
    addChild() {
      this.actType = "ADDCHILD";
      this.nodeForm = {
        num: "",
        label: "",
        width: 120,
        height: 50,
      };
      this.formDialog = true;
    },
    editItem() {
      this.actType = "EDITITEM";
      this.nodeForm = JSON.parse(JSON.stringify(this.currNode));
      this.$set(this.nodeForm, "width", this.currNode.size[0]);
      this.$set(this.nodeForm, "height", this.currNode.size[1]);
      this.$delete(this.nodeForm, "children");
      this.formDialog = true;
    },
    delItem() {
      this.$confirm("确认删除此节点及其子节点？")
        .then((_) => {
          // 删除的是父节点
          if (this.currNode.id === this.currentSelectNode.id) {
            this.myTree = {};
            this.graph.changeData(this.myTree);
            this.$emit("deleteTree", nodeId);
            return;
          }
          this.graph.removeItem(this.currNode.id);
          this.removeNode(this.myTree, this.currNode.id);
          this.removeNode(this.currentSelectNode, this.currNode.id);
          let myTree = { ...this.myTree };
          // this.$emit("updateTree", myTree);
          this.$emit("deleteTree", nodeId);
        })
        .catch((_) => {});
    },
    resetGraphData(data) {
      this.graph.clear();
      this.graph.data(data);
      this.graph.render();
      // 适应 dom 容器
      // this.graph.fitView();
    },
    registerNode() {
      let that = this;
      // 文字超出的话末尾换成 ...  官网示例中有
      const fittingString = (str, maxWidth, fontSize) => {
        const ellipsis = "...";
        const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
        let currentWidth = 0;
        let res = str;
        const pattern = new RegExp("[\u4E00-\u9FA5]+"); // 区分汉字和字母
        str.split("").forEach((letter, i) => {
          if (currentWidth > maxWidth - ellipsisLength) return;
          if (pattern.test(letter)) {
            currentWidth += fontSize;
          } else {
            // 根据fontSize获取单个字母的宽度
            currentWidth += G6.Util.getLetterWidth(letter, fontSize);
          }
          if (currentWidth > maxWidth - ellipsisLength) {
            res = `${str.substr(0, i)}${ellipsis}`;
          }
        });
        // console.log(res,'res')
        return res;
      };

      // 文字超出宽度则换行，若高度也不够 则在末尾显示 ...
      function formatText(text, maxWidth, maxHeight, fontSize) {
        const words = text.split("");
        let formattedText = ""; // 已经拼接好的文字
        let currentHeight = G6.Util.getTextSize(words[0], fontSize)[1]; // 当前高度
        let currentLine = ""; // 当前行文字
        let currentWidth = 0; // 当前行宽度
        for (let i = 0; i < words.length; i++) {
          const word = words[i];
          const wordWidth = G6.Util.getLetterWidth(word, fontSize);
          currentLine += word;
          currentWidth += wordWidth;
          if (currentWidth + wordWidth > maxWidth) {
            currentLine += "\n";
            formattedText += currentLine;
            currentLine = ""; // 重置当前行
            currentWidth = 0; // 重置当前宽度
            currentHeight += G6.Util.getTextSize(word, fontSize)[1]; // 增加当前高度
            // console.log(text, "currentHeight", currentHeight, maxHeight);
            // 如果当前高度超过最大高度，则结束循环
            if (currentHeight > maxHeight) {
              if (
                G6.Util.getTextSize(
                  currentLine.substr(currentLine.length - 1, 1),
                  fontSize
                )[0] > G6.Util.getTextSize("...", fontSize)[0]
              ) {
                formattedText = formattedText.slice(0, -1) + "...";
              } else {
                formattedText = formattedText.slice(0, -2) + "...";
              }
              break;
            }
          } else {
            if (i === words.length - 1) {
              formattedText += currentLine;
            }
          }
          // 添加单词到当前行，保留单词前的空格
        }

        return formattedText;
      }

      // 文本超出隐藏 (字段, 最大长度, 字体大小) https://blog.csdn.net/kaimo313/article/details/126889346
      // https://blog.csdn.net/qq_45796592/article/details/132637275
      G6.registerNode("card-node", {
        draw: function drawShape(cfg, group) {
          const r = 4;
          const w = cfg.size[0];
          const h = cfg.size[1];
          const data = cfg;
          // 画一个 长方体 容器
          const shape = group.addShape("rect", {
            attrs: {
              x: -w / 2,
              y: -h / 2,
              width: w,
              height: h,
              // 圆角
              radius: r,
              // 填充颜色
              fill: "#fff",
              shadowOffsetX: 0,
              shadowOffsetY: 4,
              shadowColor: "rgba(0,0,0,0.1)",
              shadowBlur: 8,
            },
            name: "main-box",
            draggable: true,
          });
          // 画一个长方形容器，用来放 数值
          group.addShape("rect", {
            attrs: {
              x: -w / 2,
              y: -h / 2,
              width: w,
              height: 24,
              fill: "#3670F5",
              radius: [r, r, 0, 0],
            },
            name: "top-box",
            draggable: true,
          });
          // 把数值放在 容器内
          const cardNumberText = group.addShape("text", {
            attrs: {
              fontSize: 12,
              lineHeight: 22,
              // 超出显示...
              // text: fittingString(data.num, w + 2, 12),
              text: "",
              fill: "#fff",
            },
            name: "institution-title",
            draggable: true,
          });
          const cardNumberTextBox = cardNumberText.getBBox();
          cardNumberText.attr({
            x: -w / 2 + 4,
            y: -h / 2 + cardNumberTextBox.height + 8,
          });
          // 存放label的文字
          const institutionText = group.addShape("text", {
            attrs: {
              fontSize: 12,
              lineHeight: 12,
              // 换行 和 超出显示...
              text: formatText(data.label, w - 8, h - 8 - 30, 12),
              fill: "#000",
            },
            name: "institution-title",
          });
          const institutionTextBox = institutionText.getBBox();
          institutionText.attr({
            x: -w / 2 + 4,
            y: -h / 2 + institutionTextBox.height + 30,
          });
          // 如果有子节点 则显示 收起 节点的小圆圈
          cfg.children &&
            cfg.children.length &&
            group.addShape("marker", {
              attrs: {
                x: 0,
                y: h / 2,
                r: 6,
                cursor: "pointer",
                symbol: G6.Marker.collapse,
                stroke: "#666",
                lineWidth: 1,
                fill: "#fff",
              },
              name: "collapse-icon",
            });
          return shape;
        },
        setState(name, value, item) {
          if (name === "collapsed") {
            const marker = item
              .get("group")
              .find((ele) => ele.get("name") === "collapse-icon");
            const icon = value ? G6.Marker.expand : G6.Marker.collapse;
            marker.attr("symbol", icon);
          }
        },
      });
    },
    registerEdge() {
      // https://blog.csdn.net/study_way/article/details/134142149
      G6.registerEdge("card-edge", {
        draw(cfg, group) {
          const startPoint = cfg.startPoint;
          const endPoint = cfg.endPoint;

          const { style } = cfg;
          const shape = group.addShape("path", {
            attrs: {
              stroke: style.stroke,
              path: [
                ["M", startPoint.x, startPoint.y],
                ["L", startPoint.x, (startPoint.y + endPoint.y) / 2],
                ["L", endPoint.x, (startPoint.y + endPoint.y) / 2],
                ["L", endPoint.x, endPoint.y],
              ],
            },
          });

          return shape;
        },
      });
    },
    addChildNode(data, parentNode, newNode) {
      // 父节点parentNode
      if (parentNode.depth >= 9) {
        this.$message.error("不能大于10级");
        return;
      }
      // 在父节点的children数组中添加新节点
      if (!(parentNode.children instanceof Array)) {
        parentNode.children = [];
      }
      parentNode.children.push(newNode);
    },
    getNewTree(obj) {
      if (obj.children) {
        obj.children.map((item) => {
          if (item.width) {
            item.width = Number(item.width);
          }
          item.width = Number(item.width);
          if (item.children && item.children.length > 0) {
            this.getNewTree(item.children);
          }
        });
      }

      return obj;
    },
    updateNode(data, nodeId, updates) {
      // 深度优先搜索来查找并更新节点
      function dfs(node) {
        if (node.id === nodeId) {
          Object.keys(updates).forEach((key) => {
            node[key] = updates[key];
          });
          return true;
        }
        if (node.children) {
          for (let child of node.children) {
            if (dfs(child)) return true;
          }
        }
        return false;
      }
      dfs(data);
    },
    removeNode(data, nodeId) {
      // 深度优先搜索来查找并删除节点

      function dfs(node, parentId) {
        if (!node.children) return null;
        const index = node.children.findIndex((child) => child.id === nodeId);
        if (index !== -1) {
          // 删除找到的节点
          node.children.splice(index, 1);
          return true;
        }
        for (let child of node.children) {
          if (dfs(child, node.id)) return true;
        }
        return false;
      }

      dfs(data, null);
    },
    myTreeToTreeData() {
      this.currentSelectNode.num = this.myTree.num;
      this.currentSelectNode.label = this.myTree.label;
      this.currentSelectNode.size = this.myTree.size;
      this.currentSelectNode.children.forEach((item, i) => {
        item.id = this.myTree.children[i].id;
        item.num = this.myTree.children[i].num;
        item.label = this.myTree.children[i].label;
        item.size = this.myTree.children[i].size;
      });
    },

    // 生成一个树中没有的id
    generateUniqueId(data, existingIds = new Set()) {
      // 遍历当前节点的所有子节点，并收集它们的ID
      function collectIds(node) {
        if (node.id) {
          existingIds.add(node.id);
        }
        if (node.children) {
          for (const child of node.children) {
            collectIds(child);
          }
        }
      }

      // 从根节点开始收集ID
      collectIds(data);

      // 生成一个新的ID并确保它是唯一的
      let newId = "1"; // 你可以根据需要调整初始ID的值或格式
      while (existingIds.has(newId)) {
        // 这里简单地通过递增数字来生成新的ID，但你可以根据需要使用更复杂的策略
        newId = (parseInt(newId) + 1).toString();
      }

      return newId;
    },
  },
};
</script>

<style scoped>
.graphContainer {
  position: relative;
  width: 100%;
  height: 100%;

  .fangda {
    position: absolute;
    cursor: pointer;
    right: 10px;
    top: 10px;
    height: 30px;
    width: 30px;
  }
}
</style>
